/*
Copyright (c) 2012 Eric Fellheimer <felly@alum.mit.edu>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 
IN THE SOFTWARE.
*/

// DISCLAIMER: I am not a financial planner (IANAFP). This is not financial advice. I am not liable for how
// you use this program. Use entirely at your own risk. Bugs may exist.

// Asset (Al)locator: http://code.google.com/p/asset-al-locator/ 
// Author: Eric Fellheimer <felly@alum.mit.edu>

// This spreadsheet apps script takes as input a set of fund buckets (taxable, 401k, Roth IRA, etc.)
// and the values of funds within them, and computes an "optimal" allocation into the set of funds.
// From there, it "locates" the funds into the buckets based on fund availability (401k options are
// often limited) and tax efficiency. This computation takes into account effective, after-tax values.

// How to use:
// 1. Enter inputs values in the blocks below
// 2. Run the update() function

// TODO
// - allocate future money for taxable account flow (without selling in taxable account?) / handle different flow rates into accounts(?)
// - add to open source

// Future improvements:
// - import data from spreadsheet, not code (optionally?)
// - auto import direct from vanguard/mint/bank/etc.
// - fund allocator slow: tweak optimizer, run in background/offline, cache results (?)
// - non-greedy asset location algorithm: more globally optimal. take into account fund choices and expense ratios(?)
// - optimize asset allocation and location simultaneously

////////////////////////////////////////////////////
// All of your inputs go in this block. 

// Fund --> Asset class breakdown
// This is a list of all the possible funds available. For each fund, specify its
// asset class breakdown and tax efficiency. Funds can be "discouraged" from use
// during allocation optimization.
var FUNDS = {
  Total_Stock_Index: {breakdown: {"US_Stocks": 1}, eff: 5}, 
  Total_Intl_Index: {breakdown: {"Global_Stock": 1}, eff: 6},
  Total_Bond_Index: {breakdown: {"Bonds": 1}, eff: 1}, 
  REIT_Index: {breakdown: {"REIT": 1}, eff: 0}, 
  Global_REIT: {breakdown: {"REIT": 1}, eff: -1, discouraged: true},
  Small_Cap_Value_Index: {breakdown: {"Small_Value_Tilt": 1}, eff: 3}, 
  Emerging_Markets_Index: {breakdown: {"Emerging_Tilt": 1}, eff: 4},
  Money_Market: {breakdown: { "Cash": 1 }, eff: 2},
  Pacific_Stock_Index: {breakdown: { "Global_Stock": 1}, eff: 3, discouraged: true},
  Extended_Index: {breakdown: {"US_Stocks": 1}, eff: 3, discouraged: true},
  Index_500: {breakdown: {"US_Stocks": 1}, eff: 3, discouraged: true}
 };

// Bucket --> Investments
// For each bucket (eg, 401k) give the tax-effective multiplier, the tax efficiency, and the asset breakdown.
// Put $0 in the asset breakdown if you don't currently have assets in the fund/bucket, but the combination
// could be used.
var BUCKETS = { 
  Taxable: { mult: .83, eff: 0,
             assets: {Total_Stock_Index: 100, Total_Intl_Index:300, Total_Bond_Index: 50, Emerging_Markets_Index: 20, 
             Money_Market: 200, Global_REIT: 0, Small_Cap_Value_Index: 0}}, 
  P_401k: { mult: .70, eff: 1,
            assets: {Extended_Index: 20, Index_500: 30, Money_Market: 0, 
                      REIT_Index: 30, Total_Bond_Index: 25, 
                      Total_Intl_Index: 100}},
  Roth_401k: {mult: 1, eff: 2,
              assets: {Extended_Index: 20, Index_500: 30, Money_Market: 0,
                      REIT_Index: 100, Total_Bond_Index: 100, Total_Intl_Index: 50}}, 
  Roth_IRA: {mult: 1, eff: 3,
             assets: {Emerging_Markets_Index: 50, Pacific_Stock_Index: 40, Global_REIT: 10, Small_Cap_Value_Index: 90,
                      Total_Stock_Index: 0, Total_Intl_Index: 0, Total_Bond_Index: 0, REIT_Index: 0}}
 };

// Class --> Desired percent
// Here you give the available asset classes and the desired percentage of each in your allocation.
// For each asset class, specify a "high-level" asset class. For example, Small cap value index
// fund's high-level asset could be "US stocks".
var ASSET_CLASSES = {
  US_Stocks : { desired: 15, high: "Stocks"},
  Small_Value_Tilt: {desired: 5, high: "Stocks"},
  Global_Stock: {desired: 15, high: "Stocks"},
  Emerging_Tilt: {desired: 5, high: "Stocks"},
  REIT: {desired: 10, high: "REIT"},
  Bonds: {desired: 30, high: "Bonds"}, 
  Cash: {desired: 20, high: "Cash"}
 };

// These are the high-level asset classes.
var HIGH_LEVEL_ASSET_CLASSES = {
  Stocks: "DUMMY",
  Bonds: "DUMMY",
  REIT: "DUMMY",
  Cash: "DUMMY"
}
    
// How many iterations to run on the asset allocation optimizer.
var OPTIMIZER_ITERATIONS = 200;

////////////////////////////////////////////////////

var DOLLAR = "$#,##0.00";

function validateData() {
  for (var fund in FUNDS) {
    for (var asset_class in FUNDS[fund].breakdown) {
      assert(ASSET_CLASSES[asset_class].desired >= 0, asset_class + " must be an asset class");
    }
  }
  
  var asset_breakdown_sum = 0;
  for (var asset_class in ASSET_CLASSES) { asset_breakdown_sum += ASSET_CLASSES[asset_class].desired; }
}

// Set after-tax effective total
function setBucketTotals() {
  var total_effective = 0;
  for (var bucket in BUCKETS) {
    var mult = BUCKETS[bucket].mult;
    var sum = 0;
    for (var fund in BUCKETS[bucket].assets) {
      sum += BUCKETS[bucket].assets[fund];
    }
    var effective = sum * mult;
    total_effective += effective
    BUCKETS[bucket].effective_sum = effective;
    BUCKETS[bucket].nominal_sum = sum;
    Logger.log("Bucket %s has effective value %s", bucket, effective);
  }
  Logger.log("Total effective = %s", total_effective);
  return total_effective;
}

function computeAssetAllocation(total_effective) {
  // eg: Total stock --> 20%, Total intl --> 40%
  asset_allocation = {};
  for (var asset_class in ASSET_CLASSES) { asset_allocation[asset_class] = 0; }

  for (var bucket in BUCKETS) { // eg 401k
    var mult = BUCKETS[bucket].mult;
    for (var fund in BUCKETS[bucket].assets) { // eg. Small cap index

      for (var asset_class in FUNDS[fund].breakdown) {
        asset_allocation[asset_class] += (BUCKETS[bucket].assets[fund] * mult) * FUNDS[fund].breakdown[asset_class];
      }
    }
  }
  
  for (var asset_class in asset_allocation) {
    asset_allocation[asset_class] = asset_allocation[asset_class] / total_effective;
  }
  return asset_allocation;
}

// Show bucket (account) data.
function printBucketData(row, sheet, total_effective) {
  first = true;
  for (var bucket in BUCKETS) {
    var col = 1;
    if (first) { 
      writeHeader(sheet, row, 1, "Account");
      writeHeader(sheet, row, 2, "Nominal");
      writeHeader(sheet, row, 3, "Multiplier");
      writeHeader(sheet, row, 4, "After-Tax Effective Value");
      row++;
      first = false;
    }
    sheet.getRange(row, 1).setValue(bucket);
    sheet.getRange(row, 2).setValue(BUCKETS[bucket].nominal_sum).setNumberFormat(DOLLAR);
    sheet.getRange(row, 3).setValue(BUCKETS[bucket].mult);
    sheet.getRange(row, 4).setValue(BUCKETS[bucket].effective_sum).setNumberFormat(DOLLAR);
    row++;
  }
  row++;
  
  writeHeader(sheet, row, 1, "Total Effective assets");
  sheet.getRange(row, 2).setValue(total_effective).setNumberFormat(DOLLAR);
  row++; row++;
  return row;
}

// Show all funds broken down by bucket
function printBucketFundData(row, sheet, fund_location) {
  var first = true;
  row++;
  for (var fund in FUNDS) {
    var col = 1;
    if (first) { writeHeader(sheet, row - 1, col, "Fund"); }
    sheet.getRange(row, col++).setValue(fund);
    for (var bucket in BUCKETS) {
      var val = BUCKETS[bucket].assets[fund];
      if (first) { writeHeader(sheet, row - 1, col, bucket); }
      if (val) { sheet.getRange(row, col).setValue(val).setNumberFormat(DOLLAR); }
      else { sheet.getRange(row, col).setValue("0"); }
      col++;  if (first) { writeHeader(sheet, row - 1, col, "Ideal"); }
      var ideal = fund_location[bucket][fund];
      if (ideal) { sheet.getRange(row, col).setValue(ideal).setNumberFormat(DOLLAR); }
      col++;  if (first) { writeHeader(sheet, row - 1, col, "Error"); } 
      col++;
    }
    row++; first = false;
  }
  row++;
  return row;
}

// "Locate" funds from the optimal fund allocation into the fund buckets using
// a naive, greedy approach.
// Currently returns values in effective dollars.
function greedyBucketFundLocations(optimal_fund_allocation, total_effective) { 
  var fund_location = {};
  // Initialize return value.
  for (var b in BUCKETS) {
    fund_location[b] = {};
    for (var f in FUNDS) {
      fund_location[b][f] = 0;
    }
  }
  
  // Initialize funds_remaining.
  var funds_remaining = {};
  var sorted_funds = sortKeysBy(FUNDS, function(f1, f2) { return FUNDS[f1].eff - FUNDS[f2].eff; });
  for (var i = 0; i < sorted_funds.length; i++) {
    var f = sorted_funds[i];
    funds_remaining[f] = optimal_fund_allocation[f];
  }
  var buckets_by_tax_eff = sortKeysBy(BUCKETS, function(b1, b2) { return BUCKETS[b2].eff - BUCKETS[b1].eff; });
  
  var total_remaining = total_effective;
  Logger.log("Locating a total of " + buckets_by_tax_eff.length + " buckets");
  Logger.log("Buckets by tax eff: " + buckets_by_tax_eff);
  
  outer:
  for (var bucket_index = 0; bucket_index < buckets_by_tax_eff.length; bucket_index++) {
    var cur_bucket = buckets_by_tax_eff[bucket_index]; // taxable, roth IRA, etc.
    var bucket_remaining = BUCKETS[cur_bucket].effective_sum;
    Logger.log("Start locating " + cur_bucket + " at index " + bucket_index + " of " + buckets_by_tax_eff.length + ". Bucket total eff: " + bucket_remaining);
    
    for (var fund in funds_remaining) {
      if (!BUCKETS[cur_bucket].assets.hasOwnProperty(fund)) {
        Logger.log("Skipping greedy location to fund " + fund + " in bucket " + cur_bucket);
        continue; 
      }
      //Logger.log("Looking at location of " + fund + " in bucket " + cur_bucket);
      var fund_remaining = funds_remaining[fund];
    
      if (bucket_remaining > fund_remaining) {
        // Allocate entire remaining of fund to this bucket
        bucket_remaining -= fund_remaining;
        fund_location[cur_bucket][fund] += fund_remaining;
        total_remaining -= fund_remaining;
        funds_remaining[fund] = 0;
        Logger.log("Locating " + fund_remaining + " to " + fund + " in " + cur_bucket);
      } else {
        // Finish filling this bucket with the fund.
        funds_remaining[fund] -= bucket_remaining;
        fund_location[cur_bucket][fund] += bucket_remaining;
        total_remaining -= bucket_remaining;
        Logger.log("Locating " + bucket_remaining + " to " + fund + " in " + cur_bucket + " - now done with " + cur_bucket);
        
        continue outer;
      }
    }
    // assert(Math.abs(bucket_remaining) < .01);
    // Logger.log("Now done locating " + cur_bucket);
    
    // Convert back to nominal values and checksum.
    for (var b in BUCKETS) {
      mult = BUCKETS[b].mult;
      ideal_bucket_sum = 0;
      for (var f in FUNDS) {
        fund_location[b][f] /= mult;
        ideal_bucket_sum += fund_location[b][f];
      }
      assert(Math.abs(ideal_bucket_sum - BUCKETS[b].nominal_sum) < .01, "Ideal sum in " + b + " doesn't match: " + ideal_bucket_sum);
    }
  }
  
  Logger.log("Funds remaining: " + Utilities.jsonStringify(funds_remaining));
  for (var fund in funds_remaining) {
    var rem = funds_remaining[fund];
    assert(Math.abs(rem) < .01, "Fund " + fund + " not located. Remaining: " + rem);
  }
  assert(Math.abs(total_remaining) < .01, "Total remaining not 0: " + total_remaining + " - Funds remaining " + Utilities.jsonStringify(funds_remaining));
  return fund_location;
}
// Asset allocation of current investments and optimum.
function printAssetAllocation(row, sheet, asset_allocation, optimal_allocation) {
  var start_allocation_row = row;
  writeHeader(sheet, row, 1, "Asset Class");
  writeHeader(sheet, row, 2, "Actual Percentage");
  writeHeader(sheet, row, 3, "Desired Percentage");
  writeHeader(sheet, row, 4, "Error");
  writeHeader(sheet, row, 5, "Optimal Allocation");
  writeHeader(sheet, row, 6, "Optimal Error");
  row++;
  var total_actual = 0;
  var total_desired = 0;
  for (var asset_class in asset_allocation) {
    sheet.getRange(row, 1).setValue(asset_class);
    sheet.getRange(row, 2).setValue(asset_allocation[asset_class]).setNumberFormat("0.0%");
    sheet.getRange(row, 3).setValue(ASSET_CLASSES[asset_class].desired / 100).setNumberFormat("0.0%");
    var error = asset_allocation[asset_class] - ASSET_CLASSES[asset_class].desired / 100 ;
    var acceptable_error = (ASSET_CLASSES[asset_class].desired / 100) * 0.25;
    var error_cell = sheet.getRange(row, 4).setValue(error).setNumberFormat("0.0%");
    if (Math.abs(error) > acceptable_error) { error_cell.setBackgroundColor("orange"); }
    
    sheet.getRange(row, 5).setValue(optimal_allocation[asset_class]).setNumberFormat("0.0%");
    optimal_error = optimal_allocation[asset_class] - ASSET_CLASSES[asset_class].desired / 100;
    optimal_error_cell = sheet.getRange(row, 6).setValue(optimal_error).setNumberFormat("0.0%");
    if (Math.abs(optimal_error) > acceptable_error) { optimal_error_cell.setBackgroundColor("orange"); }
    row++;
    
    total_actual += asset_allocation[asset_class];
    total_desired += ASSET_CLASSES[asset_class].desired / 100;
  }
  var end_row = row;
  assertNear(total_actual, 1, .001);
  assertNear(total_desired, 1, .001);
  
  //sheet.getRange(row,2).setFormula("=SUM(B" + start_allocation_row + ":B" + (row - 1) + ")").setNumberFormat("0.0%");
  //sheet.getRange(row,3).setFormula("=SUM(C" + start_allocation_row + ":C" + (row - 1) + ")").setNumberFormat("0.0%");
  
  // Pretty chart time:
  Logger.log(end_row + "   " + start_allocation_row);
  var builder = sheet.newChart().addRange(sheet.getRange(start_allocation_row, 1, end_row - start_allocation_row, 2))
    .setChartType(Charts.ChartType.PIE)
    .setOption('title', 'Current Asset Allocation')
    .setOption('is3D', 'true')
    .setPosition(start_allocation_row + 16, 8, 3, 3);
  sheet.insertChart(builder.build());
  
  row++; row++;
  return row;
}

function assetsToHighLevel(allocation) {
  var high_level_allocation = {};
  for (var high_level_asset in HIGH_LEVEL_ASSET_CLASSES) { high_level_allocation[high_level_asset] = 0; }
  
  for (var asset in allocation) {
    high_level_asset = ASSET_CLASSES[asset].high;
    high_level_allocation[high_level_asset] += allocation[asset];
  }
  return high_level_allocation;
}

function getDesiredHighLevel() {
  var high_level_allocation = {};
  for (var high_level_asset in HIGH_LEVEL_ASSET_CLASSES) { high_level_allocation[high_level_asset] = 0; }
  
  for (var asset in ASSET_CLASSES) {
    high_level_asset = ASSET_CLASSES[asset].high;
    high_level_allocation[high_level_asset] += ASSET_CLASSES[asset].desired / 100;
  }
  return high_level_allocation;
}

// High-level Asset allocation of current investments.
// TODO: code dupe with printAssetAllocation
function printHighLevelAssetAllocation(row, sheet, asset_allocation, optimal_allocation) {
  var start_allocation_row = row;
  writeHeader(sheet, row, 1, "High Level Asset Class");
  writeHeader(sheet, row, 2, "Actual Percentage");
  writeHeader(sheet, row, 3, "Desired Percentage");
  writeHeader(sheet, row, 4, "Error");
  writeHeader(sheet, row, 5, "Optimal Allocation");
  writeHeader(sheet, row, 6, "Optimal Error");
  row++;
  var total_actual = 0;
  var total_desired = 0;
  
  var asset_allocation = assetsToHighLevel(asset_allocation);
  var optimal_allocation = assetsToHighLevel(optimal_allocation);
  var desired_allocation = getDesiredHighLevel();
  for (var asset_class in asset_allocation) {
    sheet.getRange(row, 1).setValue(asset_class);
    sheet.getRange(row, 2).setValue(asset_allocation[asset_class]).setNumberFormat("0.0%");
    sheet.getRange(row, 3).setValue(desired_allocation[asset_class]).setNumberFormat("0.0%");
    var error = asset_allocation[asset_class] - desired_allocation[asset_class] ;
    var acceptable_error = desired_allocation[asset_class] * 0.25;
    var error_cell = sheet.getRange(row, 4).setValue(error).setNumberFormat("0.0%");
    if (Math.abs(error) > acceptable_error) { error_cell.setBackgroundColor("orange"); }
    
    sheet.getRange(row, 5).setValue(optimal_allocation[asset_class]).setNumberFormat("0.0%");
    optimal_error = optimal_allocation[asset_class] - desired_allocation[asset_class];
    optimal_error_cell = sheet.getRange(row, 6).setValue(optimal_error).setNumberFormat("0.0%");
    if (Math.abs(optimal_error) > acceptable_error) { optimal_error_cell.setBackgroundColor("orange"); }
    row++;
    
    total_actual += asset_allocation[asset_class];
    total_desired += desired_allocation[asset_class];
  }
  var end_row = row;
  assertNear(total_actual, 1, .001);
  assertNear(total_desired, 1, .001);
  
  //sheet.getRange(row,2).setFormula("=SUM(B" + start_allocation_row + ":B" + (row - 1) + ")").setNumberFormat("0.0%");
  //sheet.getRange(row,3).setFormula("=SUM(C" + start_allocation_row + ":C" + (row - 1) + ")").setNumberFormat("0.0%");
  
  // Pretty chart time:
  Logger.log(end_row + "   " + start_allocation_row);
  var builder = sheet.newChart().addRange(sheet.getRange(start_allocation_row, 1, end_row - start_allocation_row, 2))
    .setChartType(Charts.ChartType.PIE)
    .setOption('title', 'Current High Level Asset Allocation')
    .setOption('is3D', 'true')
    .setPosition(start_allocation_row + 2, 8, 3, 3);
  sheet.insertChart(builder.build());
  
  row++; row++;
  return row;
}

function getEffectiveFundValue(fund) {
  var total = 0;
  for (var bucket in BUCKETS) {
    var mult = BUCKETS[bucket].mult;
    var nominal = BUCKETS[bucket].assets[fund];
    if (!nominal) continue;
    total += nominal * mult;
  }
  return total;
}

// Desired vs. actual fund allocations
function printFundAllocation(row, sheet, total_effective, optimal_fund_value) {
  var start_allocation_row = row;
  writeHeader(sheet, row, 1, "Fund");
  writeHeader(sheet, row, 2, "Effective Dollars");
  writeHeader(sheet, row, 3, "Effective Percentage");
  writeHeader(sheet, row, 4, "Desired Effective value");
  row++;
  var start_row = row;
  var total_percent = 0;
  for (var fund in FUNDS) {
    sheet.getRange(row, 1).setValue(fund);
    
    var fund_effective = getEffectiveFundValue(fund);
    sheet.getRange(row, 2).setValue(fund_effective).setNumberFormat(DOLLAR);
    var percent = fund_effective / total_effective;
    total_percent += percent;
    sheet.getRange(row, 3).setValue(percent).setNumberFormat("0.0%");
    sheet.getRange(row, 4).setValue(optimal_fund_value[fund]).setNumberFormat(DOLLAR);
    row++;
  }
  assertNear(total_percent, 1, .001);
  //sheet.getRange(row,3).setFormula("=SUM(C" + start_row + ":C" + (row - 1) + ")");
  row++; row++;
  return row;
}

function update() {
  validateData();
  var total_effective = setBucketTotals();
  var asset_allocation = computeAssetAllocation(total_effective);
  var optimal_fund_allocation = find_optimal_fund_allocation(total_effective, OPTIMIZER_ITERATIONS);
  var optimal_fund_location = greedyBucketFundLocations(optimal_fund_allocation, total_effective);
  var optimal_asset_allocation = optimalAssetAllocFromFundAlloc(optimal_fund_allocation, total_effective);
  var sheet = openOrCreateSheet("Results_gen");
  
  // Force clear.
  sheet.clear();
  clearFormatting(sheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns()));
  charts = sheet.getCharts()
  for (var i in charts) { sheet.removeChart(charts[i]); }

  var row = 1;
  row = printBucketFundData(row, sheet, optimal_fund_location);
  row = printHighLevelAssetAllocation(row, sheet, asset_allocation, optimal_asset_allocation);
  row = printAssetAllocation(row, sheet, asset_allocation, optimal_asset_allocation);
  row = printFundAllocation(row, sheet, total_effective, optimal_fund_allocation);
  row = printBucketData(row, sheet, total_effective);
  
  // cleanup();
}

function cleanup() {
  if (sheet.getLastColumn() < sheet.getMaxColumns()) {
   sheet.deleteColumns(sheet.getLastColumn() + 1, sheet.getMaxColumns() - sheet.getLastColumn() - 1);
  }
  if (sheet.getLastRow() < sheet.getMaxRows()) {
    sheet.deleteRows(sheet.getLastRow() + 1, sheet.getMaxRows() - sheet.getLastRow() - 1);
  }
}

function optimalAssetAllocFromFundAlloc(fund_alloc, total_effective) {
  // TODO: duplicated code with computeAssetAllocationFromFundList
  asset_allocation = {};
  for (var asset_class in ASSET_CLASSES) { asset_allocation[asset_class] = 0; }
  
  for (var fund in FUNDS) { // eg. Small cap index
    for (var asset_class in FUNDS[fund].breakdown) {
      asset_allocation[asset_class] += fund_alloc[fund] * FUNDS[fund].breakdown[asset_class];
    }
  }  
  
  for (var asset_class in asset_allocation) {
    asset_allocation[asset_class] = asset_allocation[asset_class] / total_effective;
  }
  return asset_allocation;
}

function computeAssetAllocationFromFundList(total_effective, fund_list, fund_vals) {
  asset_allocation = {};
  for (var asset_class in ASSET_CLASSES) { asset_allocation[asset_class] = 0; }
  
  for (var i = 0; i < fund_list.length; i++) { // eg. Small cap index
    var fund = fund_list[i];
    for (var asset_class in FUNDS[fund].breakdown) {
       asset_allocation[asset_class] += fund_vals[i] * FUNDS[fund].breakdown[asset_class];
    }
  }  
  
  for (var asset_class in asset_allocation) {
    asset_allocation[asset_class] = asset_allocation[asset_class] / total_effective;
  }
  return asset_allocation;
}

function fundsToList(funds) {
  var list = [];
  for (var fund in funds) {
    if (funds[fund].discouraged != true) { list.push(fund); }
  }
  return list;
}

function fundListToJSON(fund_list, funds) {
  var fund_alloc = {};
  var i = 0;
  for (var fund in funds) {
    if (funds[fund].discouraged != true) { fund_alloc[fund] = fund_list[i++]; }
    else { fund_alloc[fund] = 0; }
  }
  return fund_alloc;
}

function find_optimal_fund_allocation(total_effective, max_func_calls) {
  fund_list = fundsToList(FUNDS);
  
  var NUM_FUNDS = fund_list.length;  // # of total funds
  var fund_vals = new Array(NUM_FUNDS);
  var NUM_CONSTRAINTS = 2;  // number of constraints
  var fake_effective = 100;
  
  // Various Cobyla constants, see Cobyla docs in Cobyja.js
  var rhobeg = 5.0;
  var rhoend = 1.0e-6;
  var iprint = 0;
  var maxfun = max_func_calls;

  var it = 0;
  function allocation_score(n,m,x,con) {
    // Constraint: total funds == total_effective, each fund is >= 0.
    var total_val = 0; for (var i = 0; i < x.length; i++) { total_val += x[i]; }
    con[0] = fake_effective - total_val; 
    con[1] = -con[0];
    //for (var i = 2; i < 2 + NUM_FUNDS; i++) { con[i] = -x[i]; }
    
    // Minimize error of asset allocation.
    possible_allocation = computeAssetAllocationFromFundList(fake_effective, fund_list, x);
    var total_error = 0;
    for (var asset_class in ASSET_CLASSES) {
      var error = (ASSET_CLASSES[asset_class].desired / 100) - possible_allocation[asset_class];
      total_error += Math.abs(error);
      //total_error += error * error;
    }
    //Logger.log("total_error=" + total_error + ", con[0]=" + con[0]);
    return total_error;
  }
  for (var i = 0; i < NUM_FUNDS; i++) { fund_vals[i] = fake_effective / NUM_FUNDS; }
  var r = FindMinimum(allocation_score, NUM_FUNDS,  NUM_CONSTRAINTS, fund_vals, rhobeg, rhoend,  iprint,  maxfun);
  assert(r == 0 || r == 1, "Bad return code from optimization: " + r);
  // for (var i = 0; i < NUM_FUNDS; i++) { Logger.log("fund i = " + fund_vals[i]); } // TODO: Make this start with the current values.
  Logger.log("Result=" + r + ", funds=" + fund_vals);
  
  fund_allocation = fundListToJSON(fund_vals, FUNDS);
  for (var k in fund_allocation) { 
    fund_allocation[k] = fund_allocation[k] * (total_effective / fake_effective);
    Logger.log("giving " + fund_allocation[k] + " to " + k); 
  }
  return fund_allocation;
}

function test_cobyla() {
  var n = 2; 			// + of variables
  var x = new Array(n);
  var m = 2; 			// number of constraints
  var rhobeg = 5.0;	// Various Cobyla constants, see Cobyla docs in Cobyja.js
  var rhoend = 1.0e-6;
  var iprint = 0;
  var maxfun = 3500;

  function test(n,m,x,con) {  	// objective function
    con[0]=x[0]*x[0]+x[1]*x[1]-1 		// first inequality constraint
    con[1]=-(x[0]*x[0]+x[1]*x[1]-1)	// second inequality constraint, together an equality constraint
     return (x[0]-5)*(x[0]-5)+(x[1]-5)*(x[1]-5); // objective function 
  }
  x[0]=-7.9; x[1]=3.2;
  var r = FindMinimum(test, n,  m, x, rhobeg, rhoend,  iprint,  maxfun);
  Logger.log("Result="+x);
}

